package cn.ycc1.functionlibrary.reflection;

/**
 * Creating a Dependency Injection Framework
 * @author ycc
 * @date 2025/3/9
 * The second example you are going to run consists in designing a simple dependency injection framework. This is a system that
 * exists in Java EE, Jakarta EE, and other enterprise application frameworks. The concept is simple: instead of creating the
 * objects your application needs yourself, you delegate this task to a factory. This factory can then reflectively explore your
 * class, check if some fields need to be initialized, and if this is the case, do it for you.
 */
public class DependencyInjection {
    /**
     * What is Dependency Injection?
     * Dependency Injection is used in applications to make sure that all your business objects are correctly initialized, and
     * that all their fields receive a correct value when doing so. Initializing a set of business objects can be complex,
     * as your objects depend on other, collaborator objects, that need to be properly initialized in a specific order.
     * Dependency injection frameworks can greatly help you in that.
     *
     * This section shows you two features these frameworks give you. The first one is the concept of singleton, and the second
     * one is dependency injection itself.
     */

    /**
     * Creating a Bean Factory
     * Let us first implement a simple bean factory. That is, a factory that you can use with the following pattern.
     *
     * public record Message(String message) {
     *     public Message {
     *         Objects.requireNonNull(message);
     *     }
     * }
     *
     * void main() {
     *     BeanFactory beanFactory = BeanFactory.INSTANCE;
     *
     *     Message message1 = beanFactory.getInstanceOf(Message.class, "Hello");
     *     System.out.println("Message 1 = " + message1);
     *
     *     Message message2 = beanFactory.getInstanceOf(Message.class, "world");
     *     System.out.println("Message 2 = " + message2);
     * }
     * This class implements the singleton pattern, that you can easily implement with an enumeration. Then, it has a
     * getInstanceOf() method, that takes a class, the one you want to build an instance of, and the arguments taken by the
     * constructor of this class.
     *
     * The implementation of the BeanFactory class is quite simple.
     *
     * First, you need an array with the types of the arguments you receive. This can be done with the following stream pattern.
     *
     * Second, you need to locate the corresponding constructor of the class beanClass. If it does not exist, an exception is
     * thrown.
     *
     * And lastly, you need to invoke this constructor with the arguments you received.
     *
     * public enum BeanFactory {
     *     INSTANCE;
     *     public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
     *         try {
     *             // Creating the array of the parameters types
     *             Class<?>[] argumentsClasses =
     *                 Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
     *
     *             // Locating the corresponding constructor
     *             Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
     *
     *             // creating the bean
     *             T bean = beanConstructor.newInstance(arguments);
     *             return bean;
     *         } catch (NoSuchMethodException | InvocationTargetException |
     *                  InstantiationException | IllegalAccessException e) {
     *             throw new RuntimeException(e);
     *         }
     *     }
     * }
     * Running the previous main() method gives you the following.
     *
     * Message 1 = Message[message=Hello]
     * Message 2 = Message[message=world]
     */

    /**
     * Creating Singletons
     * A very common pattern used in enterprise application is to have your factory to create singletons. This makes sense if
     * your bean are used to access services like a database or some REST server. You can implement such a pattern in the
     * BeanFactory class.
     *
     * Let us first define the annotation you need.
     *
     * @Target(ElementType.TYPE)
     * @Retention(RetentionPolicy.RUNTIME)
     * @interface Singleton {
     * }
     * And let us create a service class, for instance DBService.
     *
     * @Singleton
     * public class DBService {
     * }
     * The DBFactory needs to be refactored to detect this @Singleton annotation, and in that case, create only one instance
     * of that class. This part is a little subtle to write, because it needs a registry, that needs to be thread-safe,
     * and used in a thread-safe way.
     *
     * Here is the class. You can see that the code you wrote in the previous iteration has been put in a private method
     * instantiateBeanClass(), to avoid have to duplicate it in the getInstanceOf() method.
     *
     * The first step consists in checking if the @Singleton annotation has been declared on the beanClass class. If this
     * annotation is not found, then the code continues as in the first version of this class.
     *
     * If the annotation is found, then you need to enforce the Singleton pattern. For that, you can use a registry,
     * implemented by a ConcurrentMap, that is a thread-safe extension of the Map interface.
     *
     * If the registry has already an instance of beanClass, then the code returns it, there is no need to build another one.
     *
     * Then you need to create a new instance of beanClass. Note that this part can be executed by several threads concurrently.
     * So at this point, you may build several instances of beanClass. You could avoid that by synchronizing all this code,
     * but that would create some contention.
     *
     * Note that the call to ConcurrentMap.putIfAbsent() is an atomic call in the case of ConcurrentMap, so you do not need to
     * synchronize this part.
     *
     * There is one caveat though: the value you passed to the ConcurrentMap.putIfAbsent() may not be the one that, in the end,
     * was put in the map. That could be the case if several threads called this ConcurrentMap.putIfAbsent() concurrently,
     * with different instances. In the end, there is a winner, and it could be another thread than yours. So to overcome this,
     * you need to call ConcurrentMap.get(), to return the singleton.
     *
     * public enum BeanFactory {
     *     INSTANCE;
     *
     *     private final ConcurrentMap<Class<?>, Object> registry = new ConcurrentHashMap<>();
     *
     *     public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
     *         try {
     *             // Checking if the @Singleton annotation in on beanClass
     *             if (beanClass.isAnnotationPresent(Singleton.class)) {
     *
     *                 // Checking if the registry has an instance beanClass
     *                 if (registry.containsKey(beanClass)) {
     *                     return (T) registry.get(beanClass);
     *                 }
     *
     *                 // Creating a new instance of beanClass
     *                 T bean = instantiateBeanClass(beanClass, arguments);
     *
     *                 // Adding this instance to the registry
     *                 // putIfAbsent() is atomic in the case of a ConcurrentMap
     *                 registry.putIfAbsent(beanClass, bean);
     *
     *                 // Returning the value that landed in the map
     *                 // It could be another one than yours
     *                 return (T) registry.get(beanClass);
     *             } else {
     *                 T bean = instantiateBeanClass(beanClass, arguments);
     *                 return bean;
     *             }
     *         } catch (NoSuchMethodException | InvocationTargetException |
     *                  InstantiationException | IllegalAccessException e) {
     *             throw new RuntimeException(e);
     *         }
     *     }
     *
     *     private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
     *             throws NoSuchMethodException, InstantiationException,
     *             IllegalAccessException, InvocationTargetException {
     *         Class<?>[] argumentsClasses =
     *                 Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
     *         Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
     *         T bean = beanConstructor.newInstance(arguments);
     *         return bean;
     *     }
     * }
     * With this class, you can run this main() method.
     *
     * void main() {
     *     BeanFactory beanFactory = BeanFactory.INSTANCE;
     *
     *     Message message = beanFactory.getInstanceOf(Message.class, "Hello");
     *     System.out.println("Message = " + message);
     *
     *     DBService dbService1 = beanFactory.getInstanceOf(DBService.class);
     *     DBService dbService2 = beanFactory.getInstanceOf(DBService.class);
     *     System.out.println("Instances of DBService are the same? " + (dbService1 == dbService2));
     * }
     * Running the previous example prints the following.
     *
     * Message = Message[message=Hello]
     * Instances of DBService are the same? true
     */

    /**
     * Injecting Dependencies
     * The last step is to enable the scanning of beanClass in search of annotated fields.
     *
     * Let us define the following annotation.
     *
     * @Target(ElementType.TYPE)
     * @Retention(RetentionPolicy.RUNTIME)
     * @interface Inject {
     * }
     * And let us define the following service, that depends on DBService.
     *
     * @Singleton
     * public class MyApplication {
     *     @Inject
     *     private DBService dbService;
     *
     *     public boolean isDBServiceSet() {
     *         return dbService != null;
     *     }
     * }
     * What you need now is for your factory to put a correct instance of DBService in the right field of MyApplication.
     * So you need to add some code in the instantiateBeanClass() private method to scan the fields, looking for the ones that
     * declare the @Inject annotation.
     *
     * Let us refactor this private method to do the following.
     *
     * The code to construct the bean from beanClass and arguments is the same. What you get at this point is a bean, but all
     * its fields are null.
     *
     * The next step is to get all the declared fields of this bean (no matter their visibility modifiers), and keep the ones
     * that declare the @Inject annotation.
     *
     * Then, for each of these annotated fields, you need to instantiate the correct type, and set the field to this value.
     * Note that the instantiation uses BeanFactory, so this instantiation follows the @Singleton declaration you may have on
     * this class. Note that setAccessible(true) is called no matter what, even if the field is public. In that case, it would
     * not be needed.
     *
     * private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
     *         throws NoSuchMethodException, InstantiationException,
     *         IllegalAccessException, InvocationTargetException {
     *
     *     Class<?>[] argumentsClasses =
     *             Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
     *     Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
     *
     *     // This is the constructed bean
     *     T bean = beanConstructor.newInstance(arguments);
     *
     *     // Getting the fields of this bean
     *     Field[] fields = beanClass.getDeclaredFields();
     *
     *     // Filtering the fields that declare the @Inject annotation
     *     Field[] injectableFields =
     *             Arrays.stream(fields)
     *                     .filter(field -> field.isAnnotationPresent(Inject.class))
     *                     .toArray(Field[]::new);
     *
     *     for (Field injectableField : injectableFields) {
     *         // Getting the class of this field,
     *         // and creating an instance of this class
     *         // using BeanFactory
     *         Class<?> fieldClass = injectableField.getType();
     *         Object fieldValue = BeanFactory.INSTANCE.getInstanceOf(fieldClass);
     *
     *         // Setting this field to this value
     *         injectableField.setAccessible(true);
     *         injectableField.set(bean, fieldValue);
     *     }
     *     return bean;
     * }
     * You can then run the following code to check if everything has been properly set.
     *
     * void main() {
     *     BeanFactory beanFactory = BeanFactory.INSTANCE;
     *
     *     MyApplication app = BeanFactory.INSTANCE.getInstanceOf(MyApplication.class);
     *     System.out.println("App has been created with a DBService: " + app.isDBServiceSet());
     * }
     * Running the previous code prints the following.
     *
     * App has been created with a DBService: true
     */
}
